fix(engine): fail on a null cwd, and route agent workdir filesystem checks through caps - #74
Conversation
…unction Move the large block of per-node config validation logic from `validate_all` into a dedicated `validate_node_configs` function, reducing the main validation function by over 300 lines and improving readability. The extracted function handles sub-workflow, fan-out, memory, dedup, and approval node config checks, keeping the same validation behaviour. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Introduce validation for node configurations by adding a new module and importing its validation function, ensuring that node configs are checked alongside existing validations. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
…g back A null value for `cwd` or `working_dir` now fails the node with a clear error, rather than being treated as if the key were absent. This prevents a step from silently running in a different directory when an expression resolves to null because the upstream node failed or a key moved. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add two end-to-end tests that verify the agent correctly fails a step when the `cwd` expression resolves to `null`, rather than silently falling back to the working directory or treating the value as absent. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Introduces a new public enum `WorkdirCheck` and a default method `resolve_workdir` on the `AgentRunner` trait, allowing hosts to resolve and validate a node's declared working directory against their own filesystem. The engine previously had no way to check path existence or containment on a remote or sandboxed filesystem, so this change delegates that responsibility to the host while preserving backward compatibility through a default implementation that returns `Unmanaged`. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The `WorkdirCheck` type was added to the agent module but not re-exported from the caps module, making it inaccessible to external consumers. This change adds it to the public re-export list so it can be used by callers of the caps API. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Extract the filesystem-independent shape validation (absolute-vs-relative and `..` traversal) into a new `check_shape` function that runs before any filesystem access, and route the existence and directory checks through `AgentRunner::resolve_workdir` when a harness claims the workspace. This ensures the containment check that matters most cannot be accidentally dropped by a host implementation running agents on a remote filesystem. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
The `resolve_working_dir` function and `child_workspace` function were changed from synchronous to asynchronous to support the new async signature of `resolve_node_dir`, which now requires a capability reference for agent workspace resolution. This ensures that working directory resolution properly awaits the underlying filesystem checks and capability validation. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add test coverage for the new `AgentRunner` capability that allows remote hosts to resolve workspace directories without touching the local filesystem. The tests verify that a harness can answer for its own workspace, that refusals fail with the correct reason, that unmanaged answers fall back to local disk, and that path traversal checks still run before the harness is consulted. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
…kdir resolution Added two new paragraphs to the Node-Catalog page that clarify how the engine handles workdir resolution. The first explains that an expression resolving to null fails the step rather than silently falling back to a default directory, preventing steps from running in an unintended checkout. The second describes how the engine checks the shape of a declared directory but delegates filesystem existence checks to the agent runner, with the shell node taking a separate path through the shell runner and script policy. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
Expanded the changelog entry for the workdir resolution change to clarify that a null expression now fails the step, and added a new entry documenting the `AgentRunner::resolve_workdir` and `caps::WorkdirCheck` interface for harnesses that need to answer for their own filesystem. Auto-committed-on: dragonfly Co-authored-by: Medulla <medulla@tinyhumans.ai>
|
Warning Review limit reachedYour included review limit has been reached. You’re in a promotional period — use the checkbox below to run this review for free:
On-demand reviews are free for the next 28 days. After that, they cost $0.25 per reviewed file. How can I continue?Run this review now using the option above, or comment You can also wait for the limit to reset (next review available in 13 minutes), then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (13)
Warning Your free Security trial is over. An organization admin can activate billing to continue. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
How this change flows3 changed behaviours across 12 relationships. 5 surrounding behaviours are shown (60 graph nodes walked). 38 further behaviours left out to keep the diagram readable. flowchart LR
n0["run_turn_indexed<br/>changed"]:::changed
n1["child_workspace<br/>changed"]:::changed
n2["run_child<br/>changed"]:::changed
n3["map"]:::impacted
n4["assemble"]:::impacted
n5["run_graph"]:::impacted
n6["Result"]:::impacted
n7["NodeContext"]:::impacted
n0 -->|calls| n3
n0 -->|calls| n4
n0 -->|uses| n6
n0 -->|uses| n7
n1 -->|calls| n3
n2 -->|calls| n1
n2 -->|calls| n3
n4 -->|calls| n3
n4 -->|uses| n6
n4 -->|uses| n7
n5 -->|calls| n3
n5 -->|tests| n3
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 74b8cfca91
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| #[tokio::test] | ||
| async fn a_cwd_expression_that_resolves_to_null_fails_the_step() { |
There was a problem hiding this comment.
Move the new tests into a
_tests.rs file
Adding these Rust tests to tests/agent_workdir_e2e.rs leaves them in a filename that does not end in _tests.rs, contrary to the repository's explicit test-file naming requirement; rename or split this integration-test target so the file uses the required suffix.
AGENTS.md reference: AGENTS.md:L6-L7
Useful? React with 👍 / 👎.
Follow-up to #73, which merged before its review threads were worked. Four
threads, three addressed and one declined with evidence.
1. A
cwdexpression resolving tonullwas silently ignored (P1 — fixed)Thread
PRRT_kwDOTLXaD86bfFah,src/nodes/integration/agent_request.rs:200.Confirmed against
main.declared_working_dirread the key ascfg.get(key).filter(|v| !v.is_null()), andcfgarrives alreadyexpression-resolved — so
"cwd": "=nodes.prepare.item.json.worktree"whoseupstream path is missing became JSON
nulland read as absent. The loop thenfell through to
working_dir, then to the agent definition's own directory,then to whatever the harness defaults to. The step runs in a different checkout
and says nothing, which is exactly the failure the feature exists to remove.
A present-but-null value now fails the node, with a message that says why.
Regression tests:
a_cwd_expression_that_resolves_to_null_fails_the_stepanda_null_cwd_does_not_fall_back_to_working_dir(the second pins thecwd→working_dirfall-through specifically).sub_workflow'sconfig.workspacewas checked for the same bug and does nothave it: its null filter runs on the raw config before
expr::resolve, so anexpression resolving to null reaches
.as_str()and already errors. Left alone.2. Test filename policy (P1 — declined,
--no-resolve)Thread
PRRT_kwDOTLXaD86bfFaj,tests/agent_workdir_e2e.rs:1.AGENTS.md L6-L7 reads: "Keep Rust tests in files whose names end in
_tests.rs; do not add large inline test modules to production source files."The second clause is what the rule is for, and the repo enforces it exactly
there: 127 of 127 test files under
src/end in_tests.rs, with noexceptions.
tests/follows a different, equally consistent convention — the integrationtarget is named for the scenario it drives:
_e2e.rs(data_flow_e2e.rs,error_recovery_e2e.rs,approval_async_e2e.rs,reliability_e2e.rs,hitl_e2e.rs, …)_tests.rsfuzz_*.rs/smoke_all_nodes.rs/reference_workflows.rsRenaming one file to
agent_workdir_e2e_tests.rswould make it the onlyintegration target in the repo with that suffix and would not move the
directory any closer to consistent. If the maintainer wants
tests/covered bythe rule, that is a sweep of 37 files plus an AGENTS.md clarification, not a
change to this one file — happy to do it as its own PR. Left for a human to
call.
3.
src/validate.rsover the 500-line limit (P1 — fixed)Thread
PRRT_kwDOTLXaD86bfFam. Confirmed: 598 lines against the AGENTS.md L3-L5limit of 500.
Split at a real seam rather than at line 500.
validate_allis two kinds ofcheck interleaved: graph shape (duplicate ids, trigger count, edge integrity,
voidtopology,conditionrouting, declared inputs) and per-kind nodeconfig (
sub_workflowchild reference, per-item fan-out selectors,memoryscope,
dedupkey,approvalenums). The config half never looks at an edge —every check in it is one node in isolation — so it moved wholesale to
src/validate/node_config.rsbeside the existingagents/loops/scatter/workdirsubmodules.src/validate.rs598 → 277;src/validate/node_config.rs344. Public surfaceunchanged (
validate,validate_all,unresolved_agent_refs), no testchanges needed.
4. Route the filesystem checks through a
capscapability (Major — implemented)Thread
PRRT_kwDOTLXaD86bfIBo,src/workdir.rs:127.The point holds. The engine's own
shellnode is the counter-example thatproves it:
shellhandsconfig.cwdto theShellRunneruntouched, andcontainment happens inside the host's
ScriptPolicyundercaps/host/. Theagent path did the same job with
std::fs::canonicalizein the engine, so anAgentRunnerowning a remote or containerized workspace had everycwdrejected before its harness saw the request — and the workspace itself failed
first, at
workspace.canonicalize(). The existing mitigation (a run with noworkspace resolves nothing) only covers a host that never pins one; it does
nothing for a host with a valid remote workspace, which is precisely the case
raised.
Implemented, split by what is actually an outside-world effect:
..traversal — is string arithmetic withno filesystem in it. Factored out as
workdir::check_shapeand run first, onevery host, always. Keeping it in the engine means a host implementation
cannot accidentally drop the containment check that matters most.
AgentRunner::resolve_workdir(workspace, declared) -> WorkdirCheck, withthree answers:
Resolved(path)(the host's canonical path, which it assertsis contained),
Refused(reason)(fails the step with that reason, prefixedwith the node), and
Unmanaged.Unmanagedis the default method body, so no existingAgentRunnerchangesand no existing behaviour changes: the engine checks its own disk exactly as
before. That also avoids adding a field to
Capabilities, which would havebroken all ~19 struct-literal construction sites and every downstream host. It
fits the trait's existing design —
run,resolve_agent,resolve_contextandresolve_toolsare all defaulted the same way, each degrading to thepre-existing behaviour.
resolve_node_dirbecameasync(a remote check is I/O); the three call siteswere already in async fns.
sub_workflow'sconfig.workspacegoes through thesame seam.
Five new unit tests in
src/workdir_tests.rscover a harness-owned remoteworkspace resolving, a harness refusal,
Unmanagedfalling back to the localfilesystem, and the shape check running before the harness is consulted (a
host cannot bless a
..escape).Also
wiki/Node-Catalog.md"Where a step runs" documents the null rule and thefilesystem split.
CHANGELOG.md— the working-directory entry is still under[Unreleased], soit was amended in place rather than given a
Fixedsection, andAgentRunner::resolve_workdir/caps::WorkdirCheckadded.node_config.rsat344,
agent_request.rsat 475).Commands run
All clean. Test counts, baseline → this branch:
cargo testcargo test --all-features+4 unit tests (
workdir_tests), +2 e2e (agent_workdir_e2e, behindmock).No failures, no regressions.